Skip to content

Add Notion and Google Drive connector routes#205

Merged
ishaanxgupta merged 6 commits into
mainfrom
codex-add-notion-drive-connectors
May 29, 2026
Merged

Add Notion and Google Drive connector routes#205
ishaanxgupta merged 6 commits into
mainfrom
codex-add-notion-drive-connectors

Conversation

@ishaanxgupta
Copy link
Copy Markdown
Member

Summary

  • add authenticated connector status/start/disconnect routes for Notion and Google Drive
  • add OAuth authorization URL generation with no secret values in returned URLs
  • register the connector router and settings for provider client IDs/secrets/redirect URIs
  • add focused API tests for list, start, callback, and disconnect behavior

Tests

  • ENVIRONMENT=test .venv/Scripts/python.exe -m pytest tests/api/test_connectors.py
  • ENVIRONMENT=test .venv/Scripts/python.exe -m py_compile src/api/routes/connectors.py src/api/app.py src/config/settings.py tests/api/test_connectors.py

Security

  • no OAuth client secrets are returned to clients
  • token exchange and durable token storage are intentionally not implemented in this PR
  • existing untracked uv.lock was left untouched

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

Warnings
⚠️

📦 This PR changes 355 lines (additions + deletions). Large PRs are harder to review thoroughly — consider splitting it.

Messages
📖

📝 No CHANGELOG.md update detected. If this PR introduces a user-visible change, please add an entry.

📖

✅ Targeting main. Please squash commits before merging to keep the git history clean.

Generated by 🚫 dangerJS against d703470

@github-actions
Copy link
Copy Markdown
Contributor

✅ Staging Deployment Report

Item Value
Branch codex-add-notion-drive-connectors
Commit 9ab58f9
Environment Staging
Health http://3.6.255.148:8001/health
API Docs http://3.6.255.148:8001/docs
Smoke Tests success

🟢 Staging is live and healthy! Test your changes at the staging URL above.

Ready to ship? Comment /promote on this PR to merge to main and deploy to production.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new connector OAuth routing system for external knowledge sources, specifically Notion and Google Drive, along with configuration settings and tests. The review feedback highlights several important improvements: migrating from in-memory dictionaries to a persistent store like Redis to support multi-process production environments, handling OAuth errors and redirecting users back to the frontend during callbacks, removing the unused 'pending' state, and cleaning up expired states to prevent memory leaks.

Comment thread src/api/routes/connectors.py Outdated
Comment on lines +97 to +98
_pending_states: Dict[str, PendingOAuthState] = {}
_connections: Dict[str, StoredConnection] = {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using in-memory dictionaries (_pending_states and _connections) for state and connection tracking will fail in multi-process (e.g., multiple uvicorn/gunicorn workers) or multi-instance (e.g., load-balanced containers/servers) production environments. The state generated on one worker will not be available if the callback hits a different worker. Consider using a shared/persistent store (such as Redis or the existing database store) for production deployments.

Comment on lines +218 to +243
async def connector_oauth_callback(
connector_id: str,
code: str = Query(..., min_length=1),
state: str = Query(..., min_length=1),
) -> dict:
connector = _get_connector(connector_id)
pending = _pending_states.pop(state, None)
if not pending or pending.connector_id != connector.id or pending.expires_at <= _now():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid or expired connector authorization state",
)

# Token exchange and source ingestion are intentionally separate follow-up steps.
# This callback validates the flow and records a pending connection marker only.
_connections[_connection_key(pending.user_id, connector.id)] = StoredConnection(
connector_id=connector.id,
user_id=pending.user_id,
connected_at=_now(),
scopes=connector.scopes,
)
return {
"status": "connected",
"connector_id": connector.id,
"detail": f"{connector.name} authorization received",
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Improve the OAuth callback handler to:

  1. Handle errors and denials gracefully: If the user cancels or denies the authorization request, the OAuth provider redirects with an error query parameter instead of a code. Making code optional and checking for error prevents a raw 422 Unprocessable Entity validation error.
  2. Redirect to the frontend: Instead of returning a raw JSON response (which leaves the user stranded on a blank API page), redirect them back to the frontend application using RedirectResponse with the status and connector ID.
async def connector_oauth_callback(
    connector_id: str,
    state: str = Query(..., min_length=1),
    code: Optional[str] = Query(None),
    error: Optional[str] = Query(None),
) -> RedirectResponse:
    connector = _get_connector(connector_id)

    if error or not code:
        return RedirectResponse(
            url=f"{settings.frontend_url}/connectors?status=error&error={error or 'access_denied'}&connector_id={connector.id}"
        )

    pending = _pending_states.pop(state, None)
    if not pending or pending.connector_id != connector.id or pending.expires_at <= _now():
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid or expired connector authorization state",
        )

    # Token exchange and source ingestion are intentionally separate follow-up steps.
    # This callback validates the flow and records a pending connection marker only.
    _connections[_connection_key(pending.user_id, connector.id)] = StoredConnection(
        connector_id=connector.id,
        user_id=pending.user_id,
        connected_at=_now(),
        scopes=connector.scopes,
    )
    return RedirectResponse(
        url=f"{settings.frontend_url}/connectors?status=success&connector_id={connector.id}"
    )

Comment on lines +10 to +11
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, Field
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import RedirectResponse from fastapi.responses to support redirecting the user back to the frontend application after the OAuth callback completes.

Suggested change
from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel, Field
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi.responses import RedirectResponse
from pydantic import BaseModel, Field

router = APIRouter(prefix="/api/connectors", tags=["Connectors"])

ConnectorId = Literal["notion", "google-drive"]
ConnectorState = Literal["connected", "not_connected", "pending"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ConnectorState literal includes "pending", but this state is never returned by _status_for or used anywhere in the status responses. If pending status is not needed, consider removing it from the Literal type to keep the schema clean. Otherwise, implement a check in _status_for to see if there is an active pending state in _pending_states for the user.

Suggested change
ConnectorState = Literal["connected", "not_connected", "pending"]
ConnectorState = Literal["connected", "not_connected"]

Comment on lines +201 to +207
state = secrets.token_urlsafe(32)
expires_at = _now() + timedelta(minutes=STATE_TTL_MINUTES)
_pending_states[state] = PendingOAuthState(
connector_id=connector.id,
user_id=str(current_user.get("id")),
expires_at=expires_at,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent memory leaks from abandoned OAuth flows, clean up any expired states from the _pending_states dictionary whenever a new OAuth flow is initiated.

    # Clean up expired states to prevent memory leaks
    now_time = _now()
    expired_states = [k for k, v in _pending_states.items() if v.expires_at <= now_time]
    for k in expired_states:
        _pending_states.pop(k, None)

    state = secrets.token_urlsafe(32)
    expires_at = now_time + timedelta(minutes=STATE_TTL_MINUTES)
    _pending_states[state] = PendingOAuthState(
        connector_id=connector.id,
        user_id=str(current_user.get("id")),
        expires_at=expires_at,
    )

@github-actions
Copy link
Copy Markdown
Contributor

🔍 API Schema Diff

---REPORT---


Auto-generated by API Schema Diff workflow

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 29, 2026

Greptile Summary

This PR introduces Notion and Google Drive connector routes with OAuth authorization URL generation, state management, and basic disconnect stubs. It directly addresses the unauthenticated callback/connection-marking issue, the unbounded _pending_states memory leak, and the missing OAuth error-path handling that were flagged previously.

  • _prune_pending_states now enforces a TTL of 10 minutes and a MAX_PENDING_STATES cap of 1000, with overflow eviction sorted by expiry time.
  • OAuth callback no longer writes connection state — it returns {"status": "pending"} and defers token exchange, removing the previously identified path where a forged state could mark a connection.
  • Error handling in the callback now accepts error as an optional parameter and raises a clean 400 instead of the earlier 422 caused by a required code parameter.

Confidence Score: 4/5

Safe to merge with minor attention to the overflow cap and error-parameter echo in connectors.py

The previously flagged connection-forging path and memory leak are both resolved. The callback no longer writes any persistent state — it returns a stub pending response, so there is no data-integrity risk from unauthenticated access. The two remaining findings are bounded: an off-by-one that allows one extra pending state entry beyond the intended cap, and an unvalidated error string that is reflected into a JSON error body without a length ceiling.

src/api/routes/connectors.py — specifically the overflow calculation in _prune_pending_states and the error query parameter declaration in the callback handler

Important Files Changed

Filename Overview
src/api/routes/connectors.py New connector OAuth routes — addresses previously flagged unauthenticated callback and memory-leak issues; two minor off-by-one and input-validation concerns remain
tests/api/test_connectors.py Focused test suite covering list, start, callback, and disconnect flows with proper dependency overrides and autouse state cleanup
src/api/app.py Straightforward router registration; connectors_router included alongside other feature routers with no ordering concerns

Sequence Diagram

sequenceDiagram
    participant Client
    participant API as FastAPI (connectors router)
    participant States as _pending_states (in-memory)
    participant Provider as OAuth Provider

    Client->>API: "POST /{connector_id}/oauth/start (Bearer JWT)"
    API->>API: _prune_pending_states() - evict expired + overflow
    API->>API: generate state token, build authorization_url
    API->>States: "store PendingOAuthState {user_id, expires_at}"
    API-->>Client: "{authorization_url, state, expires_at}"

    Client->>Provider: redirect user to authorization_url

    alt User grants access
        Provider-->>Client: "redirect to /oauth/callback?code=X&state=S"
        Client->>API: "GET /{connector_id}/oauth/callback?code=X&state=S"
        API->>States: pop(state)
        API->>API: validate connector_id + expiry
        API-->>Client: "{status: pending} - token exchange deferred"
    else User denies or error
        Provider-->>Client: "redirect to /oauth/callback?error=access_denied&state=S"
        Client->>API: "GET /{connector_id}/oauth/callback?error=access_denied&state=S"
        API->>States: pop(state) - consumed
        API-->>Client: 400 Authorization denied
    end

    Client->>API: GET /api/connectors (Bearer JWT)
    API-->>Client: "[{id, state: not_connected}]"

    Client->>API: "POST /{connector_id}/disconnect (Bearer JWT)"
    API-->>Client: "{disconnected: false}"
Loading

Fix All in Cursor Fix All in Codex Fix All in Claude Code

Reviews (5): Last reviewed commit: "Avoid stale pending connector OAuth stat..." | Re-trigger Greptile

Comment thread src/api/routes/connectors.py
Comment thread src/api/routes/connectors.py Outdated
Comment thread src/api/routes/connectors.py Outdated
Comment thread src/api/routes/connectors.py Outdated
@github-actions
Copy link
Copy Markdown
Contributor

✅ Staging Deployment Report

Item Value
Branch codex-add-notion-drive-connectors
Commit 8c93903
Environment Staging
Health http://3.6.255.148:8001/health
API Docs http://3.6.255.148:8001/docs
Smoke Tests success

🟢 Staging is live and healthy! Test your changes at the staging URL above.

Ready to ship? Comment /promote on this PR to merge to main and deploy to production.

@github-actions
Copy link
Copy Markdown
Contributor

🔍 API Schema Diff

---REPORT---


Auto-generated by API Schema Diff workflow

Comment thread src/api/routes/connectors.py
@github-actions
Copy link
Copy Markdown
Contributor

✅ Staging Deployment Report

Item Value
Branch codex-add-notion-drive-connectors
Commit d06272b
Environment Staging
Health http://3.6.255.148:8001/health
API Docs http://3.6.255.148:8001/docs
Smoke Tests success

🟢 Staging is live and healthy! Test your changes at the staging URL above.

Ready to ship? Comment /promote on this PR to merge to main and deploy to production.

@github-actions
Copy link
Copy Markdown
Contributor

🔍 API Schema Diff

---REPORT---


Auto-generated by API Schema Diff workflow

@github-actions
Copy link
Copy Markdown
Contributor

✅ Staging Deployment Report

Item Value
Branch codex-add-notion-drive-connectors
Commit c700e1c
Environment Staging
Health http://3.6.255.148:8001/health
API Docs http://3.6.255.148:8001/docs
Smoke Tests success

🟢 Staging is live and healthy! Test your changes at the staging URL above.

Ready to ship? Comment /promote on this PR to merge to main and deploy to production.

@github-actions
Copy link
Copy Markdown
Contributor

🔍 API Schema Diff

---REPORT---


Auto-generated by API Schema Diff workflow

@github-actions
Copy link
Copy Markdown
Contributor

✅ Staging Deployment Report

Item Value
Branch codex-add-notion-drive-connectors
Commit 4fce4f3
Environment Staging
Health http://3.6.255.148:8001/health
API Docs http://3.6.255.148:8001/docs
Smoke Tests success

🟢 Staging is live and healthy! Test your changes at the staging URL above.

Ready to ship? Comment /promote on this PR to merge to main and deploy to production.

@github-actions
Copy link
Copy Markdown
Contributor

🔍 API Schema Diff

---REPORT---


Auto-generated by API Schema Diff workflow

@github-actions
Copy link
Copy Markdown
Contributor

✅ Staging Deployment Report

Item Value
Branch codex-add-notion-drive-connectors
Commit 5aa4245
Environment Staging
Health http://3.6.255.148:8001/health
API Docs http://3.6.255.148:8001/docs
Smoke Tests success

🟢 Staging is live and healthy! Test your changes at the staging URL above.

Ready to ship? Comment /promote on this PR to merge to main and deploy to production.

@github-actions
Copy link
Copy Markdown
Contributor

🔍 API Schema Diff

---REPORT---


Auto-generated by API Schema Diff workflow

@ishaanxgupta ishaanxgupta merged commit fdc13cd into main May 29, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant